I have written a very, very, very preliminary socket-based distributed object system that is intended for porting to GNU Objective-C. So far, this has been primarily an exercise to see what is actually required from the run-time in order to implement such a system.
There is one major feature missing from GNU's Objective-C run-time at the moment, which prevents this project from progressing to the more desirable very, very preliminary :-) stage. That feature is the functionality of NeXT's forward and performv, which are required in order that arbitrary messages can be captured by proxy objects and then later executed by the remote objects they stand for.
The puzzle I would like to pose to this mailing list is how such a run-time facility can best (most portably) be implemented. It doesn't have to be compatible with NeXT's forward and performv; it just needs to provide their capabilities (in fact, NeXT's API is proabably a good one to avoid).
This is a tricky puzzle, though, the underlying difficulty being that the C language does not support the run-time construction of argument lists and return values. (It doesn't really support the run-time deconstruction of argument lists either, except that we now have standard hack for it called "stdarg.h".)
This puzzle is ironically made more difficult by the portable message-dispatching implementation chosen by GNU Obj-C. The problem here is that objc_msgSend() only gets to choose a method implementation, and is helpless to change the message's "self" and "_cmd" (which are hard-compiled into the message call in the GNU run-time system). If this were not the case, a limited form of forwarding could be implemented portably.
This portability puzzle vexes NeXT's distributed object system as well, resulting in problems like not being able to return doubles from remote messages.
The benefits of distributed objects are too great, though, to abandon this quest if it turns out that no 100% pure solution exists. More portable is better, but I'd like to hear any practical ideas that will work well with the GNU compiler.
I looked at doing forwardv:: for the first release of the run time. To implement forwardv:: is a perplexing problem:
* Implementation of forwardv:: would require stack frame copying. (or at least modification of the existing stack frame.)
* The microprocessor itself poses problems such as word width, alignment, and -- possibly on some RISC machines -- parameter passing using register windows.
* Each function used to implement forwardv:: may need to know the amount of data pushed onto the stack for local variables. (So to index backward.)
* The dispatch function must know about passed arguments. While this is encoded and available, what do you do for variable arguments and structures? Oh yea, how about void*?
* The stack must be indexed to extract return addresses -- that is, if this can be done with your microprocessor of the day. There are also microprocessor/compiler specific data pushed onto the stack that the functions may need to know about such as the variable frame on 680x0 microprocessors (note the link instruction on each function entry).
At the time, these issues and others I haven't mentioned caused me think that DO reeks of assembly code that is not only tailored to the microprocessor, by the vendor's operating system as well. This is a significant maintenance issue and is in no way portable.
The other item I wanted to add to the run time is "active objects" but it suffers from many of the same issues.
I think you touched on something I suspect about the NeXT run time: it contains microprocessor/vendor specific assembly code.
I invite ideas of how to do this in a somewhat portable fashion. Portability was one of the chief goals of the run time.
-dpg
From: Steve Naroff <Steve_Naroff@next.com>
Date: Tue, 19 Jan 93 17:50:24 -0800
To: dennis_glatting@trirex.com
Subject: The forward/performv Puzzle
Cc: gnu-objc@prep.ai.mit.edu
The "style" of forwarding required for DO is hard to do in portable way (given the low level nature of C). C compilers have a hard enough time agreeing on structure layout! Nevertheless, I believe you are over estimating the assembly required implement this. The two hooks in the NeXT runtime that support forwarding are:
extern id _objc_msgForward (id self, SEL sel, ...);
extern id objc_msgSendv (id self, SEL op,
unsigned arg_size, marg_list arg_frame);
The assembly code amounts to less than 1 page for both of these routines (let me know if you want the code).
You could imagine a style of forwarding that works fine locally (and is totally portable). The interface is:
- forward:(SEL)op; // adequate for supporting delegation.
"forward:" would be implemented by any class interested in providing a delegate. It would not be directly responsible for sending the message (which is why it is more portable). This is also parallel with the way you separate "lookup from dispatch" in the GNU runtime.
regards, snaroff.
Begin forwarded message:
Date: Tue, 19 Jan 93 08:05:01 -0800
From: Dennis Glatting <dglattin@trirex.com>
To: <burchard@geom.umn.edu>
Subject: Re: The forward/performv Puzzle
Cc: gnu-objc@prep.ai.mit.edu
Reply-To: dennis_glatting@trirex.com
I looked at doing forwardv:: for the first release of the run time. To implement forwardv:: is a perplexing problem:
* Implementation of forwardv:: would require stack frame copying. (or at least modification of the existing stack frame.)
* The microprocessor itself poses problems such as word width, alignment, and -- possibly on some RISC machines -- parameter passing using register windows.
* Each function used to implement forwardv:: may need to know the amount of data pushed onto the stack for local variables. (So to index backward.)
* The dispatch function must know about passed arguments. While this is encoded and available, what do you do for variable arguments and structures? Oh yea, how about void*?
* The stack must be indexed to extract return addresses -- that is, if this can be done with your microprocessor of the day. There are also microprocessor/compiler specific data pushed onto the stack that the functions may need to know about such as the variable frame on 680x0 microprocessors (note the link instruction on each function entry).
At the time, these issues and others I haven't mentioned caused me think that DO reeks of assembly code that is not only tailored to the microprocessor, by the vendor's operating system as well. This is a significant maintenance issue and is in no way portable.
The other item I wanted to add to the run time is "active objects" but it suffers from many of the same issues.
I think you touched on something I suspect about the NeXT run time: it contains microprocessor/vendor specific assembly code.
I invite ideas of how to do this in a somewhat portable fashion. Portability was one of the chief goals of the run time.
-dpg
Date: Wed, 20 Jan 93 23:40 PST
From: michael@stb.info.com (Michael Gersten)
To: gnu-objc@prep.ai.mit.edu
Subject: Re: The forward/performv Puzzle
Here's my comments on how to forward:
The issue is how to portable forward a call to someone else, given that you don't know how far up the stack the stack frame runs, nor the format of said stack frame.
Method one:
You decide to pass a message to an object. You call the dispatcher. The dispatcher must do one of two things: It must either call (or return the address of) the specified routine, or failing that, the "forward" routine.
Assumptions used: The number of arguments declared to be taken by the routine (in this case, an arbitrary number of args from the original routine, and the probably 0 args declared by the forward routine) do not affect the format of the stack frame.
Disadvantages: Cannot retreive the arguments in the forward routine, nor can the forward routine pass those arguments to another routine.
Method two:
All method calls get another arg pushed on the stack, an invisible arg specifying how many bytes have been pushed on the stack. This is used to determine the stack size, which can then be "independently" copied
Advantages: Fairly portable (see below)
Disadvantages: Does not properly handle the case of one arg being a pointer to another arg -- you'll have instead a pointer to something back up the stack in another frame.
Still relies on the forward method knowing the stack info.
Method three:
Attempting to emulate the next mechanism:
The forward method will be called with a special argument type, that can only be used by passing it to the performv method. The performv method will use this special type, with the goal of being implementation independent, to call the routine.
In order for performv to be so independent, it must take the new object, the new method name, and the old info; it would then unwind the stack to the point indicated in the special argument, modifies the stack frame in place to substitute the new object (which may be the same as the old), and the new method, and then reruns the send routine.
This requires that the send routine, when it fails, must instead send to the forward routine, and with a slightly different stack frame -- it needs to put onto the stack a new stack frame specifying the original object, the original method, and this special argument for performv.
Advantages: It works. It's fairly portable. It doesn't break under any arg types
Disadvantages: Requires that the machine have some way to unwind a stack frame (does this conflict with register windows machines?). This code (the unwinding of the stack, and the setting up info needed to unwind) would probably be machine specific, and only used in one or two places (not really much different than requiring each machien description to provide a prologue and epilogue routine). Not completely compatible with a message dispatcher that works by returning a pointer for someone else to call -- would require that the code so involved change from
push args
push obj
push method
call lookup
jump addr
to
...
call lookup
jump NZ,addr
jump massage_stack_and_forward
other arg passing conventions may require other changes -- you need a new #define for "the name of the insn pattern to use (usually jne) after calling the method lookup routine; must not jump if a lookup failure occurs". Also not quite standard in the implementation of forward and performv -- note that performv under this system does not return to the caller (forward), but instead to the caller of forward. Since sending a message "method1" to "foo" is translated into a "forward" message, this has the result of returning directly after the call to method1, with no way for that routine to know that a failure occured (and no way for the forward routine to do something AFTER the performv, which may vary well be desireable). No easy way to check for a "double forward", where the forward routine cannot operate because of a failed send (infinite loop in the error handler). Still, it looks like the best of the implementation independent ways. Would someone like to shoot it down please?
--
Michael Gersten michael@stb.info.com
NeXT Registered Developer (NeRD) # 3860 -- Hire me! Quick!
Will program computers for food (and net connection, health benefits, cash,...)
Date: Thu, 21 Jan 93 11:53:49 -0600
From: burchard@geom.umn.edu
To: michael@stb.info.com (Michael Gersten)
Subject: Re: The forward/performv Puzzle
Cc: gnu-objc@prep.ai.mit.edu
> (does this conflict with register windows machines?)
To avoid lots of hypothetical discussions, I suggest everyone thinking about this take a look at section 14.7 of the GNU CC manual, on C calling conventions (about a dozen pages). It was an eye-opener for me---I hadn't realized that some platforms will go so far as to split a single argument into pieces, and pass part on the stack and part in a register. And don't count on there being stack space "reserved" for the missing args/pieces that are passed in registers.
The varargs facilities in GNU CC provide a bit of guidance but not a lot of actual help. The one thing that could be helpful is the builtin "function" __builtin_saveregs(). Don't try taking a pointer to this thing---it's really a slippery compiler trick to cause the insertion of some assembly code at the beginning of the routine (no matter where it's "called"). The assembly code for __builtin_saveregs() is maintained for each platform that needs it in libgcc2.c. Thankfully it isn't, in the end, all that terrible, and gives some hints about how the opposite operation (of loading arguments properly from a more uniform format) would proceed.
Still, after looking at all this, I have to regard the stack-oriented argument info maintained and published by the Obj-C runtime as being somewhat euphemistic.